/*
 * Copyright (c) 2012, 2020, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * The Universal Permissive License (UPL), Version 1.0
 *
 * Subject to the condition set forth below, permission is hereby granted to any
 * person obtaining a copy of this software, associated documentation and/or
 * data (collectively the "Software"), free of charge and under any and all
 * copyright rights in the Software, and any and all patent rights owned or
 * freely licensable by each licensor hereunder covering either (i) the
 * unmodified Software as contributed to or provided by such licensor, or (ii)
 * the Larger Works (as defined below), to deal in both
 *
 * (a) the Software, and
 *
 * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
 * one is included with the Software each a "Larger Work" to which the Software
 * is contributed by such licensors),
 *
 * without restriction, including without limitation the rights to copy, create
 * derivative works of, display, perform, and distribute the Software and make,
 * use, sell, offer for sale, import, export, have made, and have sold the
 * Software and the Larger Work(s), and to sublicense the foregoing rights on
 * either these or other terms.
 *
 * This license is subject to the following condition:
 *
 * The above copyright notice and either this complete permission notice or at a
 * minimum a reference to the UPL must be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.oracle.truffle.jx.tck;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.PolyglotException;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value;
import org.graalvm.polyglot.tck.LanguageProvider;
import org.graalvm.polyglot.tck.ResultVerifier;
import org.graalvm.polyglot.tck.Snippet;
import org.graalvm.polyglot.tck.TypeDescriptor;
import org.junit.Assert;

public class SLTCKLanguageProvider implements LanguageProvider {
  private static final String ID = "sl";
  private static final String PATTERN_VALUE_FNC = "function %s() {return %s;}";
  private static final String PATTERN_BIN_OP_FNC = "function %s(a,b) {return a %s b;}";
  private static final String PATTERN_POST_OP_FNC = "function %s(a) {a %s;}";
  private static final String PATTERN_BUILTIN0 = "function %sBuiltin0() {return %s();}";
  private static final String PATTERN_BUILTIN1 = "function %sBuiltin1(a) {return %s(a);}";
  private static final String PATTERN_BUILTIN2 = "function %sBuiltin2(a, b) {return %s(a, b);}";
  private static final String[] PATTERN_STATEMENTS = {
    "function %s() {r = 0;\n%s\nreturn r;\n}", "function %s(p1) {r = 0;\n%s\nreturn r;\n}",
  };

  private static final TypeDescriptor NUMBER_RETURN =
      TypeDescriptor.union(TypeDescriptor.NUMBER, TypeDescriptor.intersection());

  @Override
  public String getId() {
    return ID;
  }

  @Override
  public Value createIdentityFunction(Context context) {
    return eval(context, "function id (a) {return a;}", "id");
  }

  @Override
  public Collection<? extends Snippet> createValueConstructors(Context context) {
    final Collection<Snippet> res = new ArrayList<>();

    res.add(
        createValueConstructor(
            context, "1 == 2", "boolean", "createBoolean", TypeDescriptor.BOOLEAN));
    res.add(createValueConstructor(context, "1", "number", "createNumber", TypeDescriptor.NUMBER));
    res.add(
        createValueConstructor(
            context,
            "9223372036854775808",
            "bigNumber",
            "createBigNumber",
            TypeDescriptor.intersection()));
    res.add(
        createValueConstructor(
            context, "\"string\"", "string", "createString", TypeDescriptor.STRING));
    Snippet.Builder opb =
        Snippet.newBuilder(
            "object",
            eval(
                context,
                "function createObject() {\n"
                    + "obj1 = new();\n"
                    + "obj1.attr = 42;\n"
                    + "return obj1;\n"
                    + "}",
                "createObject"),
            TypeDescriptor.OBJECT);
    res.add(opb.build());
    opb =
        Snippet.newBuilder(
            "function",
            eval(
                context,
                "function fn() {\n" + "}" + "function createFunction() {\n" + "return fn;\n" + "}",
                "createFunction"),
            TypeDescriptor.EXECUTABLE);

    res.add(
        createValueConstructor(
            context,
            "wrapPrimitive(1 == 2)",
            "wrapped-boolean",
            "createWrappedBoolean",
            TypeDescriptor.BOOLEAN));
    res.add(
        createValueConstructor(
            context,
            "wrapPrimitive(1)",
            "wrapped-number",
            "createWrappedNumber",
            TypeDescriptor.NUMBER));
    res.add(
        createValueConstructor(
            context,
            "wrapPrimitive(\"string\")",
            "wrapped-string",
            "createWrappedString",
            TypeDescriptor.STRING));

    res.add(
        createValueConstructor(
            context,
            "typeOf(1 == 2)",
            "boolean-metaobject",
            "createBooleanMetaObject",
            TypeDescriptor.META_OBJECT));
    res.add(
        createValueConstructor(
            context,
            "typeOf(1)",
            "number-metaobject",
            "createNumberMetaObject",
            TypeDescriptor.META_OBJECT));
    res.add(
        createValueConstructor(
            context,
            "typeOf(\"str\")",
            "string-metaobject",
            "createStringMetaObject",
            TypeDescriptor.META_OBJECT));
    res.add(
        createValueConstructor(
            context,
            "typeOf(NULL)",
            "null-metaobject",
            "createNullMetaObject",
            TypeDescriptor.META_OBJECT));
    res.add(
        createValueConstructor(
            context,
            "typeOf(new())",
            "object-metaobject",
            "createObjectMetaObject",
            TypeDescriptor.META_OBJECT));
    res.add(
        createValueConstructor(
            context,
            "typeOf(createStringMetaObject)",
            "function-metaobject",
            "createFunctionMetaObject",
            TypeDescriptor.META_OBJECT));

    res.add(opb.build());
    return Collections.unmodifiableCollection(res);
  }

  @Override
  public Collection<? extends Snippet> createExpressions(Context context) {
    final Collection<Snippet> res = new ArrayList<>();
    final Value fnc = eval(context, String.format(PATTERN_BIN_OP_FNC, "add", "+"), "add");
    Snippet.Builder opb =
        Snippet.newBuilder("+", fnc, NUMBER_RETURN)
            .parameterTypes(TypeDescriptor.NUMBER, TypeDescriptor.NUMBER);
    res.add(opb.build());
    opb =
        Snippet.newBuilder("+", fnc, TypeDescriptor.STRING)
            .parameterTypes(TypeDescriptor.STRING, TypeDescriptor.ANY);
    res.add(opb.build());
    opb =
        Snippet.newBuilder("+", fnc, TypeDescriptor.STRING)
            .parameterTypes(TypeDescriptor.ANY, TypeDescriptor.STRING);
    res.add(opb.build());
    res.add(
        createBinaryOperator(
                context, "-", "sub", NUMBER_RETURN, TypeDescriptor.NUMBER, TypeDescriptor.NUMBER)
            .build());
    res.add(
        createBinaryOperator(
                context, "*", "mul", NUMBER_RETURN, TypeDescriptor.NUMBER, TypeDescriptor.NUMBER)
            .build());
    res.add(
        createBinaryOperator(
                context, "/", "div", NUMBER_RETURN, TypeDescriptor.NUMBER, TypeDescriptor.NUMBER)
            .resultVerifier(
                (snippetRun) -> {
                  final Value dividend = snippetRun.getParameters().get(0);
                  final Value divider = snippetRun.getParameters().get(1);
                  final PolyglotException exception = snippetRun.getException();
                  if (dividend.isNumber() && divider.fitsInDouble() && divider.asDouble() == 0) {
                    Assert.assertNotNull(exception);
                  } else if (exception != null) {
                    throw exception;
                  } else {
                    Assert.assertTrue(
                        TypeDescriptor.NUMBER.isAssignable(
                            TypeDescriptor.forValue(snippetRun.getResult())));
                  }
                })
            .build());
    res.add(
        createBinaryOperator(
                context, "==", "eq", TypeDescriptor.BOOLEAN, TypeDescriptor.ANY, TypeDescriptor.ANY)
            .build());
    res.add(
        createBinaryOperator(
                context,
                "!=",
                "neq",
                TypeDescriptor.BOOLEAN,
                TypeDescriptor.ANY,
                TypeDescriptor.ANY)
            .build());
    res.add(
        createBinaryOperator(
                context,
                "<=",
                "le",
                TypeDescriptor.BOOLEAN,
                TypeDescriptor.NUMBER,
                TypeDescriptor.NUMBER)
            .build());
    res.add(
        createBinaryOperator(
                context,
                ">=",
                "ge",
                TypeDescriptor.BOOLEAN,
                TypeDescriptor.NUMBER,
                TypeDescriptor.NUMBER)
            .build());
    res.add(
        createBinaryOperator(
                context,
                "<",
                "l",
                TypeDescriptor.BOOLEAN,
                TypeDescriptor.NUMBER,
                TypeDescriptor.NUMBER)
            .build());
    res.add(
        createBinaryOperator(
                context,
                ">",
                "g",
                TypeDescriptor.BOOLEAN,
                TypeDescriptor.NUMBER,
                TypeDescriptor.NUMBER)
            .build());
    res.add(
        createBinaryOperator(
                context,
                "||",
                "or",
                TypeDescriptor.BOOLEAN,
                TypeDescriptor.BOOLEAN,
                TypeDescriptor.ANY)
            .resultVerifier(
                (snippetRun) -> {
                  final Value firstParam = snippetRun.getParameters().get(0);
                  final Value secondParam = snippetRun.getParameters().get(1);
                  final PolyglotException exception = snippetRun.getException();
                  if (firstParam.isBoolean()
                      && !firstParam.asBoolean()
                      && !secondParam.isBoolean()) {
                    Assert.assertNotNull(exception);
                  } else if (exception != null) {
                    throw exception;
                  } else {
                    Assert.assertTrue(
                        TypeDescriptor.BOOLEAN.isAssignable(
                            TypeDescriptor.forValue(snippetRun.getResult())));
                  }
                })
            .build());
    res.add(
        createBinaryOperator(
                context,
                "&&",
                "land",
                TypeDescriptor.BOOLEAN,
                TypeDescriptor.BOOLEAN,
                TypeDescriptor.ANY)
            .resultVerifier(
                (snippetRun) -> {
                  final Value firstParam = snippetRun.getParameters().get(0);
                  final Value secondParam = snippetRun.getParameters().get(1);
                  final PolyglotException exception = snippetRun.getException();
                  if (firstParam.isBoolean()
                      && firstParam.asBoolean()
                      && !secondParam.isBoolean()) {
                    Assert.assertNotNull(exception);
                  } else if (exception != null) {
                    throw exception;
                  } else {
                    Assert.assertTrue(
                        TypeDescriptor.BOOLEAN.isAssignable(
                            TypeDescriptor.forValue(snippetRun.getResult())));
                  }
                })
            .build());
    res.add(
        createPostfixOperator(
                context,
                "()",
                "callNoArg",
                TypeDescriptor.NULL,
                TypeDescriptor.executable(TypeDescriptor.ANY))
            .build());
    res.add(
        createPostfixOperator(
                context,
                "(1)",
                "callOneArg",
                TypeDescriptor.NULL,
                TypeDescriptor.executable(TypeDescriptor.ANY, TypeDescriptor.NUMBER))
            .build());
    res.add(
        createPostfixOperator(
                context,
                "(1, \"\")",
                "callTwoArgs",
                TypeDescriptor.NULL,
                TypeDescriptor.executable(
                    TypeDescriptor.ANY, TypeDescriptor.NUMBER, TypeDescriptor.STRING))
            .build());

    return Collections.unmodifiableCollection(res);
  }

  @Override
  public Collection<? extends Snippet> createStatements(Context context) {
    final Collection<Snippet> res = new ArrayList<>();
    res.add(
        createStatement(
            context,
            "if",
            "iffnc",
            "if ({1}) '{'\n{0}=1;\n'}' else '{'\n{0}=0;\n'}'",
            TypeDescriptor.NUMBER,
            TypeDescriptor.BOOLEAN));
    res.add(
        createStatement(
            context,
            "while",
            "whilefnc",
            "while ({1}) '{'break;\n'}'",
            TypeDescriptor.NUMBER,
            TypeDescriptor.BOOLEAN));
    res.add(
        createStatement(
            context, "assign", "assignfnc", "{1} = {0};", TypeDescriptor.ANY, TypeDescriptor.ANY));

    // relevant builtins
    res.add(createBuiltin(context, "getSize", TypeDescriptor.NUMBER, TypeDescriptor.ARRAY));
    res.add(createBuiltin(context, "hasSize", TypeDescriptor.BOOLEAN, TypeDescriptor.ANY));
    res.add(createBuiltin(context, "isExecutable", TypeDescriptor.BOOLEAN, TypeDescriptor.ANY));
    res.add(createBuiltin(context, "isNull", TypeDescriptor.BOOLEAN, TypeDescriptor.ANY));

    res.add(
        createBuiltin(
            context,
            "isInstance",
            TypeDescriptor.BOOLEAN,
            TypeDescriptor.META_OBJECT,
            TypeDescriptor.ANY));
    res.add(
        createBuiltin(
            context,
            "typeOf",
            TypeDescriptor.union(TypeDescriptor.META_OBJECT, TypeDescriptor.NULL),
            TypeDescriptor.ANY));

    return res;
  }

  @Override
  public Collection<? extends Snippet> createScripts(Context context) {
    final Collection<Snippet> res = new ArrayList<>();
    res.add(loadScript(context, "resources/Ackermann.sl", TypeDescriptor.NULL, null));
    res.add(loadScript(context, "resources/Fibonacci.sl", TypeDescriptor.NULL, null));
    return Collections.unmodifiableCollection(res);
  }

  @Override
  public Collection<? extends Source> createInvalidSyntaxScripts(Context context) {
    try {
      final Collection<Source> res = new ArrayList<>();
      res.add(createSource("resources/InvalidSyntax01.sl"));
      res.add(createSource("resources/InvalidSyntax02.sl"));
      return Collections.unmodifiableCollection(res);
    } catch (IOException ioe) {
      throw new AssertionError("IOException while creating a test script.", ioe);
    }
  }

  private static Snippet createValueConstructor(
      final Context context,
      final String value,
      final String id,
      final String functionName,
      final TypeDescriptor type) {
    final Snippet.Builder opb =
        Snippet.newBuilder(
            id,
            eval(context, String.format(PATTERN_VALUE_FNC, functionName, value), functionName),
            type);
    return opb.build();
  }

  private static Snippet.Builder createBinaryOperator(
      final Context context,
      final String operator,
      final String functionName,
      final TypeDescriptor type,
      final TypeDescriptor ltype,
      final TypeDescriptor rtype) {
    final Value fnc =
        eval(context, String.format(PATTERN_BIN_OP_FNC, functionName, operator), functionName);
    return Snippet.newBuilder(operator, fnc, type).parameterTypes(ltype, rtype);
  }

  private static Snippet.Builder createPostfixOperator(
      final Context context,
      final String operator,
      final String functionName,
      final TypeDescriptor type,
      final TypeDescriptor ltype) {
    final Value fnc =
        eval(context, String.format(PATTERN_POST_OP_FNC, functionName, operator), functionName);
    return Snippet.newBuilder(operator, fnc, type).parameterTypes(ltype);
  }

  private static Snippet createStatement(
      final Context context,
      final String id,
      final String functionName,
      final String expression,
      final TypeDescriptor returnType,
      TypeDescriptor... paramTypes) {
    final Object[] formalParams = new String[paramTypes.length + 1];
    formalParams[0] = "r";
    for (int i = 1; i < formalParams.length; i++) {
      formalParams[i] = "p" + i;
    }
    final String formattedExpression = MessageFormat.format(expression, formalParams);
    final Value fnc =
        eval(
            context,
            String.format(PATTERN_STATEMENTS[paramTypes.length], functionName, formattedExpression),
            functionName);
    return Snippet.newBuilder(id, fnc, returnType).parameterTypes(paramTypes).build();
  }

  private static Snippet createBuiltin(
      final Context context,
      final String builtinName,
      final TypeDescriptor returnType,
      TypeDescriptor... paramTypes) {

    String pattern;
    switch (paramTypes.length) {
      case 0:
        pattern = PATTERN_BUILTIN0;
        break;
      case 1:
        pattern = PATTERN_BUILTIN1;
        break;
      case 2:
        pattern = PATTERN_BUILTIN2;
        break;
      default:
        throw new AssertionError();
    }

    final String formattedExpression = String.format(pattern, builtinName, builtinName);
    final Value fnc = eval(context, formattedExpression, builtinName);
    return Snippet.newBuilder(builtinName, fnc, returnType).parameterTypes(paramTypes).build();
  }

  private static Snippet loadScript(
      final Context context,
      final String resourceName,
      final TypeDescriptor type,
      final ResultVerifier verifier) {
    try {
      final Source src = createSource(resourceName);
      return Snippet.newBuilder(src.getName(), context.eval(src), type)
          .resultVerifier(verifier)
          .build();
    } catch (IOException ioe) {
      throw new AssertionError("IOException while creating a test script.", ioe);
    }
  }

  private static Source createSource(final String resourceName) throws IOException {
    int slashIndex = resourceName.lastIndexOf('/');
    String scriptName = slashIndex >= 0 ? resourceName.substring(slashIndex + 1) : resourceName;
    try (Reader in =
        new InputStreamReader(
            SLTCKLanguageProvider.class.getResourceAsStream(resourceName), "UTF-8")) {
      return Source.newBuilder(ID, in, scriptName).build();
    }
  }

  private static Value eval(
      final Context context, final String fncDecl, final String functionName) {
    return context.eval(
        ID,
        fncDecl
            + "\n"
            + "function main() {\n"
            + String.format("  return %s;\n", functionName)
            + "}");
  }
}
